#region Header Licence

//  NKnobMan, Copyright 2009, Alexandre MUTEL.
// 
//  This is free software; you can redistribute it and/or modify it
//  under the terms of the GNU Lesser General Public License as
//  published by the Free Software Foundation; either version 2.1 of
//  the License, or (at your option) any later version.
// 
//  This software is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
//  Lesser General Public License for more details.
// 
//  You should have received a copy of the GNU Lesser General Public
//  License along with this software; if not, write to the Free
//  Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
//  02110-1301 USA, or see the FSF site: http://www.fsf.org.

#endregion

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Text;
using System.Windows.Forms;
using System.Xml;

namespace NKnobMan
{
    [DataContract]
    public class KnobMan : IWin32Window
    {
        private static KnobMan _defaultInstance;
        private readonly IntPtr _handle;
        private readonly string _savedKnobManTitleBar;
        private readonly Dictionary<string, string> _values;
        private Icon _icon;
        private List<Layer> _layers;

        public KnobMan(IntPtr handle)
        {
            _handle = handle;
            _values = new Dictionary<string, string>();
            _layers = new List<Layer>();
            _savedKnobManTitleBar = NativeGetText();
            InitLayers();
        }

        public Icon Icon
        {
            get
            {
                if (_icon == null)
                {
                    _icon = IconHelper.GetApplicationIconFromHandle(Handle);
                }
                return _icon;
            }
        }

        public static KnobMan Default
        {
            get
            {
                if (_defaultInstance == null)
                {
                    string[] args = Environment.GetCommandLineArgs();
                    IntPtr handle = IntPtr.Zero;

                    // Try to get the Handle from the command line 1st argument
                    if (args.Length == 2)
                    {
                        try
                        {
                            handle = new IntPtr(long.Parse(args[1]));
                        }
                        catch (Exception ex)
                        {
                        }
                        if (handle != IntPtr.Zero)
                        {
                            if (! IsValid(handle))
                            {
                                handle = IntPtr.Zero;
                            }
                        }

                        if (handle == IntPtr.Zero)
                        {
                            Log.ErrorMessageBox("Invalid Handle (" + args[1] + ")");
                        }
                    }

                    if (handle == IntPtr.Zero)
                    {
                        KnobManProcessSelectorForm selectKnobManProcess = new KnobManProcessSelectorForm();
                        if (selectKnobManProcess.SingleKnobMan || selectKnobManProcess.ShowDialog() == DialogResult.OK)
                        {
                            handle = IsValid(selectKnobManProcess.SelectedKnobManHandle)
                                         ? selectKnobManProcess.SelectedKnobManHandle
                                         : IntPtr.Zero;
                        }
                    }

                    if (handle != IntPtr.Zero)
                    {
                        _defaultInstance = new KnobMan(handle);
                    } 
                }
                return _defaultInstance;
            }
        }

        [DataMember]
        public int PrefsLayers
        {
            get { return GetValue<int>(0, "Layers"); }
            set { SetValue(0, "Layers", value); }
        }

        [DataMember]
        public int PrefsPreviewFrames
        {
            get { return GetValue<int>(0, "PreviewFrames"); }
            set { SetValue(0, "PreviewFrames", value); }
        }

        [DataMember]
        public int PrefsRenderFrames
        {
            get { return GetValue<int>(0, "RenderFrames"); }
            set { SetValue(0, "RenderFrames", value); }
        }

        [DataMember]
        public int PrefsOutputSizeX
        {
            get { return GetValue<int>(0, "OutputSizeX"); }
            set { SetValue(0, "OutputSizeX", value); }
        }

        [DataMember]
        public int PrefsOutputSizeY
        {
            get { return GetValue<int>(0, "OutputSizeY"); }
            set { SetValue(0, "OutputSizeY", value); }
        }

        [DataMember]
        public int PrefsAlign
        {
            get { return GetValue<int>(0, "Align"); }
            set { SetValue(0, "Align", value); }
        }

        [DataMember]
        public Color PrefsBackColor
        {
            get { return GetValue<Color>(0, "BgColor"); }
            private set
            {
                // NOT IMPLEMENTED : KnobMan is crashing
                // SetValue(0, "BgColor", value);
            }
        }

        [DataMember]
        public IList<Layer> Layers
        {
            get { return _layers.AsReadOnly(); }
        }

        public Layer CurrentLayer
        {
            get
            {
                NativeSetText("KnobManCtl:GetCurrentLayer");
                int layerId = int.Parse(NativeGetText());
                foreach (Layer layer in _layers)
                {
                    if (layer.Id == layerId)
                    {
                        return layer;
                    }
                }
                return null;
            }
        }

        #region IWin32Window Members

        public IntPtr Handle
        {
            get { return _handle; }
        }

        #endregion

        public void Reset()
        {
            _values.Clear();
            InitLayers();
        }

        private void InitLayers()
        {
            int countLayer = GetValue<int>(0, "Layers");
            _layers.Clear();
            for (int i = 0; i < countLayer; i++)
            {
                _layers.Add(new Layer(this, i + 1));
            }
        }

        private void RestoreKnobManTileBar()
        {
            if (!string.IsNullOrEmpty(_savedKnobManTitleBar))
            {
                Win32.SetWindowText(Handle, _savedKnobManTitleBar);
            }
        }

        public static bool IsValid(IntPtr handle)
        {
            bool isHandleFound = false;
            // Check that its a KnobMan handle
            Process[] processes = Process.GetProcessesByName("KnobMan");
            foreach (Process process in processes)
            {
                if (process.MainWindowHandle.Equals(handle))
                {
                    isHandleFound = true;
                    break;
                }
            }
            return isHandleFound;
        }

        public Layer AddLayer()
        {
            NativeSetText("KnobManCtl:AddLayer");
            InitLayers();
            return CurrentLayer;
        }

        public Layer DuplicateLayer()
        {
            NativeSetText("KnobManCtl:DupLayer");
            InitLayers();
            return CurrentLayer;
        }

        public void RemoveCurrentLayer()
        {
            NativeSetText("KnobManCtl:DelLayer");
            InitLayers();
        }

        public void OpenFile(string fileName)
        {
            FileStream fileStream = null;
            try
            {
                fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read);
                byte byte1 = (byte)fileStream.ReadByte();
                byte byte2 = (byte)fileStream.ReadByte();
                if ( byte1 != 'K' || byte2 != 'M')
                {
                    throw new Exception("Invalid knob file format");
                }
            }
            catch (Exception ex)
            {
                if ( fileStream != null)
                {
                    try
                    {
                        fileStream.Close();   
                    } catch
                    {
                    }
                }
                throw new ArgumentException("Unable to load knob file", fileName, ex);
            }

            NativeSetText("KnobManCtl:OpenFile  " + fileName);
            Reset();
        }

        public void SaveToFile(string fileName)
        {
            NativeSetText("KnobManCtl:SaveFile " + fileName);
        }

        public void Exit()
        {
            NativeSetText("KnobManCtl:Exit");
        }

        public string DumpXml()
        {
            DataContractSerializer ser = new DataContractSerializer(typeof (KnobMan));
            MemoryStream stream = new MemoryStream();
            var writer = new XmlTextWriter(new StreamWriter(stream)) {Formatting = Formatting.Indented};
            ser.WriteObject(writer, this);
            writer.Flush();
            return "Handle: " + Handle + "\r\n" + Encoding.ASCII.GetString(stream.ToArray());
        }

        public KnobImageOutput CalculateOptimizedExportRenderFramesSize()
        {
            int prefsOutputSizeX = PrefsOutputSizeX;
            int prefsOutputSizeY = PrefsOutputSizeY;
            int prefsRenderFrames = PrefsRenderFrames;

            int framePerWidth = (int) Math.Sqrt(prefsRenderFrames*(double) prefsOutputSizeY/prefsOutputSizeX);

            int framePerWidthLow = 0;
            int framePerWidthtHigh = 0;
            int framePerHeight;
            for (int i = framePerWidth; i >= 1; i--)
            {
                framePerHeight = prefsRenderFrames/i;
                if (framePerHeight*i == prefsRenderFrames)
                {
                    framePerWidthLow = i;
                    break;
                }
            }
            for (int i = framePerWidth; i < (prefsRenderFrames + 1); i++)
            {
                framePerHeight = prefsRenderFrames/i;
                if (framePerHeight*i == prefsRenderFrames)
                {
                    framePerWidthtHigh = i;
                    break;
                }
            }

            double bestSizeLow =
                Math.Sqrt(Math.Pow(framePerWidthLow*prefsOutputSizeX, 2) +
                          Math.Pow((double) prefsRenderFrames*prefsOutputSizeY/framePerWidthLow, 2));
            double bestSizeHigh =
                Math.Sqrt(Math.Pow(framePerWidthtHigh*prefsOutputSizeX, 2) +
                          Math.Pow((double) prefsRenderFrames*prefsOutputSizeY/framePerWidthtHigh, 2));

            if (bestSizeLow <= bestSizeHigh)
            {
                framePerWidth = framePerWidthLow;
            }
            else
            {
                framePerWidth = framePerWidthtHigh;
            }
            framePerHeight = prefsRenderFrames/framePerWidth;

            return new KnobImageOutput(framePerWidth, framePerHeight, framePerWidth*prefsOutputSizeX,
                                       framePerHeight*prefsOutputSizeY, prefsOutputSizeX, prefsOutputSizeY);
        }

        public void ExportImage(string fileName)
        {
            NativeSetText("KnobManCtl:ExportImage " + fileName);
            RestoreKnobManTileBar();
        }

        public void ExportOptimizedImage(string fileName)
        {
            NativeSetText("KnobManCtl:ExportImage " + fileName);
            RestoreKnobManTileBar();

            if (File.Exists(fileName))
            {
                // Preload image in memory
                FileStream stream = new FileStream(fileName, FileMode.Open);
                byte[] imageToLoad = new byte[stream.Length];
                stream.Read(imageToLoad, 0, (int) stream.Length);
                stream.Close();
                MemoryStream inputStream = new MemoryStream(imageToLoad, false);

                Bitmap sourceBitmap = new Bitmap(inputStream);
                KnobImageOutput optimizedFrameSize = CalculateOptimizedExportRenderFramesSize();
                Bitmap destination = new Bitmap(optimizedFrameSize.TotalWidth, optimizedFrameSize.TotalHeight,
                                                sourceBitmap.PixelFormat);

                using (Graphics graphics = Graphics.FromImage(destination))
                {
                    bool isSourceImageHorizontal = (sourceBitmap.Width ==
                                                    optimizedFrameSize.RenderFrames*optimizedFrameSize.FrameSizeX);
                    int totalFrameIndex = 0;
                    Rectangle destRect = new Rectangle();
                    destRect.Width = optimizedFrameSize.FrameSizeX;
                    destRect.Height = optimizedFrameSize.FrameSizeY;
                    Rectangle sourceRect = new Rectangle(Point.Empty, destRect.Size);
                    for (int y = 0; y < optimizedFrameSize.RenderFramesY; y++)
                    {
                        destRect.Y = y*optimizedFrameSize.FrameSizeY;
                        for (int x = 0; x < optimizedFrameSize.RenderFramesX; x++, totalFrameIndex++)
                        {
                            destRect.X = x*optimizedFrameSize.FrameSizeX;
                            if (isSourceImageHorizontal)
                            {
                                sourceRect.X = totalFrameIndex*optimizedFrameSize.FrameSizeX;
                                sourceRect.Y = 0;
                            }
                            else
                            {
                                sourceRect.X = 0;
                                sourceRect.Y = totalFrameIndex*optimizedFrameSize.FrameSizeY;
                            }

                            graphics.DrawImage(sourceBitmap, destRect, sourceRect, GraphicsUnit.Pixel);
                        }
                    }
                }
                destination.Save(fileName);
            }
        }


        internal T GetValue<T>(int layerId, string propertyName, string param)
        {
            object result = null;
            object defaultResult = null;

            string value = (String.IsNullOrEmpty(param)) ? "" : GetValueCached(layerId, param);

            try
            {
                if (typeof (T) == typeof (int))
                {
                    defaultResult = 0;
                    result = int.Parse(value);
                }
                else if (typeof (T) == typeof (float))
                {
                    defaultResult = 0.0f;
                    result = float.Parse(value);
                }
                else if (typeof (T) == typeof (string))
                {
                    defaultResult = "";
                    result = value;
                }
                else if (typeof (T) == typeof (bool))
                {
                    defaultResult = false;
                    result = value == "1";
                } else if (typeof(T) == typeof(Color))
                {
                    TypeConverter conv = TypeDescriptor.GetConverter(typeof(Color));
                    Color resultColor = (Color)conv.ConvertFrom(value);
                    result = Color.FromArgb(255, resultColor);
                }
            }
            catch (Exception ex)
            {
                Log.Error(
                    "Exception while parsing Property [{0}] Associated to KnobProperty [{1}]. Error Message : {2}",
                    propertyName, param, ex.Message);
            }
            return (T) (result ?? defaultResult);
        }


        internal T GetValue<T>(int layerId, string param)
        {
            return GetValue<T>(layerId, param, param);
        }

        internal void SetValue<T>(int layerId, string param, T value)
        {
            SetValue(layerId, param, param, value);
        }

        internal void SetValue<T>(int layerId, string propertyName, string param, T value)
        {
            string valueStr = null;
            if (typeof(T) == typeof(Color))
            {
                object colorValueObj = value;
                Color fromColor = (Color) colorValueObj;
                valueStr = ((fromColor.R << 16) | (fromColor.G << 8) | fromColor.B).ToString();
            } else
            {
                valueStr = value.ToString();                
            }

            SetValueCached(layerId, propertyName, param, valueStr);
        }

        private string KnobManValueParam(int layer, string param)
        {
            string result = "";
            if (layer == 0)
            {
                result += "Prefs.";
            }
            else
            {
                result += "Layer" + layer + ".";
            }
            result += param;
            return result;
        }

        internal bool NativeSetText(string command)
        {
            IntPtr result = Win32.SendMessage(new HandleRef(this, Handle), Win32.WM_SETTEXT, IntPtr.Zero, command);
            Log.Verbose("Message WM_SETTEXT: " + command);
            return result != IntPtr.Zero;
        }

        internal string NativeGetText()
        {
            int length = Win32.GetWindowTextLength(Handle);
            StringBuilder result = new StringBuilder();
            if (length > 0)
            {
                result.EnsureCapacity(length + 1);
                Win32.GetWindowText(Handle, result, length + 1);
            }
            return result.ToString();
        }

        private void SetValueCached(int layer, string propertyName, string param, string value)
        {
            if (String.IsNullOrEmpty(param))
            {
                Log.Info("No Knob Property associated with Property [{0}] ", propertyName);
            }
            else
            {
                string knobParam = KnobManValueParam(layer, param);
                _values.Remove(knobParam);
                _values.Add(knobParam, value);
                NativeSetText("KnobManCtl:SetValue " + knobParam + "=" + value);
                RestoreKnobManTileBar();
            }
        }

        private string GetValueCached(int layer, string param)
        {
            string knobParam = KnobManValueParam(layer, param);
            string command = "KnobManCtl:GetValue " + knobParam;
            string result;
            _values.TryGetValue(knobParam, out result);
            if (result == null)
            {
                NativeSetText(command);
                result = NativeGetText();
                Log.Verbose("Message WM_GETTEXT: {0} Value : [{1}]", command, result);
                RestoreKnobManTileBar();
                _values.Add(knobParam, result);
            }
            return result;
        }

        #region Nested type: KnobImageOutput

        public struct KnobImageOutput
        {
            public int FrameSizeX;
            public int FrameSizeY;
            public int RenderFramesX;
            public int RenderFramesY;
            public int TotalHeight;
            public int TotalWidth;

            public KnobImageOutput(int renderFramesX, int renderFramesY, int totalWidth, int totalHeight, int frameSizeX,
                                   int frameSizeY)
            {
                RenderFramesX = renderFramesX;
                RenderFramesY = renderFramesY;
                TotalWidth = totalWidth;
                TotalHeight = totalHeight;
                FrameSizeX = frameSizeX;
                FrameSizeY = frameSizeY;
            }

            public int RenderFrames
            {
                get { return RenderFramesX*RenderFramesY; }
            }
        }

        #endregion
    }
}